/* 
 * DataManager.java 28.06.2011
 *  
 *  
 * Candidate: Alessandra Otterbach
 * Prometric ID: Algisina1
 * Username: Algisina
 *  
 * Sun Certified Developer for Java 2 Platform, Standard Edition Programming 
 * Assignment (CX-310-252A)
 *  
 * This class is part of the Programming Assignment of the Sun Certified 
 * Developer for Java 2 Platform, Standard Edition certification program, must 
 * not be used out of this context and must be used exclusively by Sun 
 * Microsystems, Inc. 
 */
package suncertify.db;

import java.io.EOFException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import suncertify.dao.RoomRecord;
import suncertify.util.LoggingFacade;
import suncertify.util.RecordFieldReader;
import suncertify.util.SearchChecker;

/**
 * Class that implements the logic concerning the logic on the data.
 * 
 * @author Alessandra Otterbach
 * 
 */
public class DataManager {
    /** Character Encoding */
    private static final String ENCODING = "US-ASCII";

    /** The value that identifies a record as being valid */
    private static final int VALID = 0;

    private static final LoggingFacade logger = LoggingFacade
	    .getInstance(DataManager.class);

    private static String path = null;

    private static RandomAccessFile file = null;

    private static ReadWriteLock recNoLock = new ReentrantReadWriteLock();

    /** Map that stores all the valid records, the key is the record number */
    private Map<Integer, String[]> roomMap = new HashMap<Integer, String[]>();

    /**
     * Constructor being called with the path of the database file.
     * 
     * @param dbPath
     * @throws IOException
     */
    public DataManager(String dbPath) throws IOException {
	logger.info("DataManager: " + dbPath, "entering constructor");
	path = dbPath;
	file = new RandomAccessFile(path, "rw");
	// Initial map fill
	readAll();
    }

    /**
     * Reads a record from the file. Returns an array where each element is a
     * record value.
     * 
     * @param recNo
     *            number of the record to be read
     * @return Array with record values
     * @throws RecordNotFoundException
     */
    public String[] read(int recNo) throws RecordNotFoundException,
	    DataException {

	logger.info("read(): " + recNo, "entering method");
	final byte[] input = new byte[RoomRecord.ROOM_RECORD_LENGTH];

	// Keep synchronized block as small as possible, to keep
	// the time, that the file is blocked as small as possible.
	synchronized (file) {
	    try {
		file.seek(RoomRecord.HEADER_LENGTH + (recNo - 1)
			* RoomRecord.ROOM_RECORD_LENGTH);
		file.readFully(input);
	    } catch (IOException e) {
		if (e instanceof EOFException)
		    throw new RecordNotFoundException("EOFException in read()");
		else {
		    throw new DataException("IOException in read()", e);
		}
	    }
	}

	RecordFieldReader readRecord = new RecordFieldReader(input);
	try {
	    int deleted = readRecord.readByte();
	    String name = readRecord.read(RoomRecord.NAME_FIELD_LENGTH);
	    String location = readRecord.read(RoomRecord.LOCATION_FIELD_LENGTH);
	    String size = readRecord.read(RoomRecord.SIZE_FIELD_LENGTH);
	    String smoking = readRecord.read(RoomRecord.SMOKING_FIELD_LENGTH);
	    String rate = readRecord.read(RoomRecord.RATE_FIELD_LENGTH);
	    String date = readRecord.read(RoomRecord.DATE_FIELD_LENGTH);
	    String owner = readRecord.read(RoomRecord.OWNER_FIELD_LENGTH);

	    String[] roomRecordResult = null;
	    if (deleted == VALID) {
		roomRecordResult = new String[7];
		roomRecordResult[0] = name;
		roomRecordResult[1] = location;
		roomRecordResult[2] = size;
		roomRecordResult[3] = smoking;
		roomRecordResult[4] = rate;
		roomRecordResult[5] = date;
		roomRecordResult[6] = owner;
	    } else {
		throw new RecordNotFoundException();
	    }

	    logger.info("read()", "exiting");
	    return roomRecordResult;
	} catch (UnsupportedEncodingException exc) {
	    throw new DataException("UnsupportedEncodingException in read()",
		    exc);
	} catch (IOException e) {
	    throw new DataException("IOException in read()", e);
	}
    }

    /**
     * Modifies the fields of a record. The new value for field n appears in
     * data[n]. Throws SecurityException if the record is locked with a cookie
     * other than lockCookie.
     * 
     * @param recNo
     *            number of the record to be updated
     * @param data
     *            record to be updated
     * @throws RecordNotFoundException
     * @throws SecurityException
     */
    public void update(int recNo, String[] data) throws DataException {
	logger.info("update(): " + recNo, "entering method");
	long offset = RoomRecord.HEADER_LENGTH + RoomRecord.ROOM_RECORD_LENGTH
		* (recNo - 1);
	// Save record
	persist(offset, recNo, data);
	logger.info("update()", "exiting");
    }


    /**
     * Stores data to the file at the given position. Updates map with all records.
     * 
     * @param offset
     * @param recNo
     * @param data
     * @throws DataException
     */
    private void persist(long offset, int recNo, String[] data)
	    throws DataException {
	logger.info("persist(): " + recNo, "entering method");
	try {

	    // Keep synchronized block as small as possible, to keep
	    // the time, that the file is blocked as small as possible.
	    synchronized (file) {
		// Put pointer at right position.
		file.seek(offset);
		writeData(data, file);
	    }

	    // Map update
	    recNoLock.writeLock().lock();
	    roomMap.put(recNo, data);
	    logger.info("persist()", "exiting");
	} catch (IOException e) {
	    throw new DataException("IOException in persist()", e);
	} finally {
	    recNoLock.writeLock().unlock();
	}
    }

    /**
     * Creates a new record in the database (possibly reusing a deleted entry).
     * Inserts the given data, and returns the record number of the new record.
     * 
     * @param data
     *            record to be created
     * @return record number of the new record
     * @throws DuplicateKeyException
     */
    public int create(String[] data) throws DuplicateKeyException,
	    DataException {

	// First see if there are any deleted entries, that can be used for the
	// new entry
	int recNo = checkDeletedEntries();

	try {
	    long offset;
	    // If recNo is 0, a new record entry must be put at the end of the
	    // file
	    if (recNo == 0) {
		offset = file.length();
		// recNo is number of records in file plus one.
		recNo = getRecNos() + 1;
	    } else {
		offset = RoomRecord.HEADER_LENGTH
			+ RoomRecord.ROOM_RECORD_LENGTH * (recNo - 1);
	    }

	    // Save record
	    persist(offset, recNo, data);
	} catch (IOException e) {
	    throw new DataException("IOException in create()", e);
	}

	return recNo;
    }

    /**
     * Deletes a record, making the record number and associated disk storage
     * available for reuse. Throws SecurityException if the record is locked
     * with a cookie other than lockCookie.
     * 
     * @param recNo
     *            number of the record to be deleted
     * @param lockCookie
     *            cookie, this record should be locked with
     * @throws RecordNotFoundException
     * @throws SecurityException
     */
    public void delete(int recNo, long lockCookie) {
	long offset = RoomRecord.HEADER_LENGTH + RoomRecord.ROOM_RECORD_LENGTH
		* (recNo - 1);
	try {
	    // Keep synchronized block as small as possible, to keep
	    // the time, that the file is blocked as small as possible.
	    synchronized (file) {
		// Put pointer at right position.
		file.seek(offset);
		file.writeByte(1);
	    }

	    // Map update
	    recNoLock.writeLock().lock();
	    roomMap.put(recNo, null);
	} catch (IOException e) {
	    throw new DataException("IOException in create()", e);
	} finally {
	    recNoLock.writeLock().unlock();
	}
    }

    /**
     * Returns an array of record numbers that match the specified criteria.
     * Field n in the database file is described by criteria[n]. A null value in
     * criteria[n] matches any field value. A non-null value in criteria[n]
     * matches any field value that begins with criteria[n]. (For example,
     * "Fred" matches "Fred" or "Freddy".)
     * 
     * @param criteria
     *            search criteria
     * @return an array of record numbers
     */
    public int[] find(String[] criteria) {
	logger.info("find()" + criteria, "entering method");
	int[] recNosFound;
	List<Integer> listRecNosFound = new ArrayList<Integer>();

	SearchChecker checker = new SearchChecker();

	// First check, if there is any criteria. If not, all records
	// match.
	if (checker.checkCriteriaEmpty(criteria)) {
	    listRecNosFound.addAll(roomMap.keySet());
	} else {
	    for (int i = 1; i <= roomMap.size(); i++) {

		String[] record = roomMap.get(i);

		// null would be a deleted record.
		if (record != null && checker.checkCriteria(criteria, record)) {
		    listRecNosFound.add(i);
		}
	    }
	}

	Collections.sort(listRecNosFound);
	int sizeFoundRecNos = listRecNosFound.size();
	recNosFound = new int[sizeFoundRecNos];

	for (int j = 0; j < sizeFoundRecNos; j++) {
	    recNosFound[j] = listRecNosFound.get(j);
	}
	logger.info("find()", "exiting");
	return recNosFound;
    }

    /**
     * Checks, if there is a deleted entry.
     * 
     * @return the recNo of the first found deleted entry or 0 if there is none.
     */
    private int checkDeletedEntries() {
	int recNo = 0;

	for (int i = 1; i <= roomMap.size(); i++) {
	    if (roomMap.get(i) == null) {
		return i;
	    }
	}

	return recNo;
    }

    /**
     * Writes the data to the file. For every attribute a paddedArray with the
     * particular size is created, the data gets encoded, copied to the
     * paddedArray and written to the file.
     * 
     * @param data
     *            the reord data
     * @param raf
     *            the file, the data is written to
     * @throws IOException
     */
    private void writeData(String[] data, RandomAccessFile raf)
	    throws IOException {
	// * 1: name <br>
	// * 2: location <br>
	// * 3: size <br>
	// * 4: smoking <br>
	// * 5: rate <br>
	// * 6: date <br>
	// * 7: owner <br>

	// Deleted Flag
	raf.writeByte(0);

	// // Name
	byte[] paddedByteArray = new byte[RoomRecord.NAME_FIELD_LENGTH];
	byte[] stringByteArray = data[0].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);
	// Location
	stringByteArray = data[1].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Size
	paddedByteArray = new byte[RoomRecord.SIZE_FIELD_LENGTH];
	stringByteArray = data[2].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Smoking
	paddedByteArray = new byte[RoomRecord.SMOKING_FIELD_LENGTH];
	stringByteArray = data[3].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Rate
	paddedByteArray = new byte[RoomRecord.RATE_FIELD_LENGTH];
	stringByteArray = data[4].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Date
	paddedByteArray = new byte[RoomRecord.DATE_FIELD_LENGTH];
	stringByteArray = data[5].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);

	// Owner
	paddedByteArray = new byte[RoomRecord.OWNER_FIELD_LENGTH];
	stringByteArray = data[6].trim().getBytes(ENCODING);
	System.arraycopy(stringByteArray, 0, paddedByteArray, 0,
		stringByteArray.length);
	raf.write(paddedByteArray);
    }

    /**
     * Check if record exists
     * 
     * @return true, if record exists
     */
    public boolean checkRecordExists(int recNo) {
	if (roomMap.get(recNo) == null) {
	    return false;
	}
	return true;
    }

    /**
     * Provides the number of records over all. If the record map is filled, the
     * method returns the size of the map. Otherwise the file is read and the
     * number of records in the file is returned.
     * 
     * @return number of records
     * @throws IOException
     */
    private int getRecNos() throws IOException {
	int numberRecNos;

	if (roomMap.isEmpty()) {
	    // Length of file minus Header (74) divided record length (160)
	    numberRecNos = (int) ((file.length() - RoomRecord.HEADER_LENGTH) / RoomRecord.ROOM_RECORD_LENGTH);
	} else {
	    try {
		// Lock map
		recNoLock.readLock().lock();
		numberRecNos = roomMap.size();
	    } finally {
		recNoLock.readLock().unlock();
	    }
	}

	return numberRecNos;
    }

    /**
     * Only called in the constructor of DataManager to build the map containing
     * the records. 
     * 
     * @return
     * @throws RecordNotFoundException
     * @throws IOException
     */
    private List<String[]> readAll() throws IOException {
	List<String[]> allRooms = new ArrayList<String[]>();
	int recNos = getRecNos();

	// While filling the map, we do not want any other thread to change the
	// map, so we lock it.
	recNoLock.writeLock().lock();
	try {

	    for (int i = 1; i <= recNos; i++) {
		String[] record;
		try {
		    record = read(i);
		    // Fill map
		    roomMap.put(i, record);
		    allRooms.add(record);
		} catch (RecordNotFoundException e) {
		    // if record not found, fill null in map
		    roomMap.put(i, null);
		}
	    }
	    return allRooms;
	} finally {
	    recNoLock.writeLock().unlock();
	}
    }

    /**
     * Clean shutdown only empties the RandomAccessFile. There is no risk to loose data, because 
     * the data is always written directly to the file.
     * 
     */
    void cleanShutdown() {
        
        file = null;
    }
}
